接下來是Dapper關鍵功能 Strongly Typed Mapping
,因為難度高,這邊會切分成多篇來解說。
第一篇先以ADO.NET DataReader GetItem By Index跟Dapper Strongly Typed Query對比,查看兩者IL的差異,了解Dapper Query Mapping的主要邏輯。
有了邏輯後,如何實作,我這邊依序用三個技術 :Reflection、Expression、Emit
從頭實作三個版本Query方法來讓讀者漸進式了解。
首先使用以下代碼來追蹤Dapper Query邏輯
class Program
{
static void Main(string[] args)
{
using (var cn = new SqlConnection(@"Data Source=(localdb)\MSSQLLocalDB;Integrated Security=SSPI;Initial Catalog=master;"))
{
var result = cn.Query<User>("select N'暐翰' Name , 25 Age").First();
Console.WriteLine(result.Name);
Console.WriteLine(result.Age);
}
}
}
public class User
{
public string Name { get; set; }
public int Age { get; set; }
}
這邊需要重點來看Dapper.SqlMapper.GenerateDeserializerFromMap
方法,它負責Mapping的邏輯,可以看到裡面大量使用Emit IL技術。
要了解這段IL邏輯,我的方式 :「不應該直接進到細節,而是先查看完整生成的IL」
,至於如何查看,這邊需要先準備 il-visualizer 開源工具,它可以在Runtime查看DynamicMethod生成的IL。
它預設支持vs 2015、2017,假如跟我一樣使用vs2019的讀者,需要注意
%USERPROFILE%\Documents\Visual Studio 2019
路徑下面.netstandard2.0
專案,需要建立netstandard2.0
並解壓縮到該資料夾最後重開visaul studio並debug運行,進到GetTypeDeserializerImpl方法,對DynamicMethod點擊放大鏡 > IL visualizer > 查看Runtime
生成的IL代碼
可以得出以下IL
IL_0000: ldc.i4.0
IL_0001: stloc.0
IL_0002: newobj Void .ctor()/Demo.User
IL_0007: stloc.1
IL_0008: ldloc.1
IL_0009: dup
IL_000a: ldc.i4.0
IL_000b: stloc.0
IL_000c: ldarg.0
IL_000d: ldc.i4.0
IL_000e: callvirt System.Object get_Item(Int32)/System.Data.IDataRecord
IL_0013: dup
IL_0014: stloc.2
IL_0015: dup
IL_0016: isinst System.DBNull
IL_001b: brtrue.s IL_0029
IL_001d: unbox.any System.String
IL_0022: callvirt Void set_Name(System.String)/Demo.User
IL_0027: br.s IL_002b
IL_0029: pop
IL_002a: pop
IL_002b: dup
IL_002c: ldc.i4.1
IL_002d: stloc.0
IL_002e: ldarg.0
IL_002f: ldc.i4.1
IL_0030: callvirt System.Object get_Item(Int32)/System.Data.IDataRecord
IL_0035: dup
IL_0036: stloc.2
IL_0037: dup
IL_0038: isinst System.DBNull
IL_003d: brtrue.s IL_004b
IL_003f: unbox.any System.Int32
IL_0044: callvirt Void set_Age(Int32)/Demo.User
IL_0049: br.s IL_004d
IL_004b: pop
IL_004c: pop
IL_004d: stloc.1
IL_004e: leave IL_0060
IL_0053: ldloc.0
IL_0054: ldarg.0
IL_0055: ldloc.2
IL_0056: call Void ThrowDataException(System.Exception, Int32, System.Data.IDataReader, System.Object)/Dapper.SqlMapper
IL_005b: leave IL_0060
IL_0060: ldloc.1
IL_0061: ret
要了解這段IL之前需要先了解ADO.NET DataReader快速讀取資料方式
會使用GetItem By Index
方式,如以下代碼
public static class DemoExtension
{
private static User CastToUser(this IDataReader reader)
{
var user = new User();
var value = reader[0];
if(!(value is System.DBNull))
user.Name = (string)value;
var value = reader[1];
if(!(value is System.DBNull))
user.Age = (int)value;
return user;
}
public static IEnumerable<User> Query<T>(this IDbConnection cnn, string sql)
{
if (cnn.State == ConnectionState.Closed) cnn.Open();
using (var command = cnn.CreateCommand())
{
command.CommandText = sql;
using (var reader = command.ExecuteReader())
while (reader.Read())
yield return reader.CastToUser();
}
}
}
接著查看此Demo - CastToUser方法生成的IL代碼
DemoExtension.CastToUser:
IL_0000: nop
IL_0001: newobj User..ctor
IL_0006: stloc.0 // user
IL_0007: ldarg.0
IL_0008: ldc.i4.0
IL_0009: callvirt System.Data.IDataRecord.get_Item
IL_000E: stloc.1 // value
IL_000F: ldloc.1 // value
IL_0010: isinst System.DBNull
IL_0015: ldnull
IL_0016: cgt.un
IL_0018: ldc.i4.0
IL_0019: ceq
IL_001B: stloc.2
IL_001C: ldloc.2
IL_001D: brfalse.s IL_002C
IL_001F: ldloc.0 // user
IL_0020: ldloc.1 // value
IL_0021: castclass System.String
IL_0026: callvirt User.set_Name
IL_002B: nop
IL_002C: ldarg.0
IL_002D: ldc.i4.1
IL_002E: callvirt System.Data.IDataRecord.get_Item
IL_0033: stloc.1 // value
IL_0034: ldloc.1 // value
IL_0035: isinst System.DBNull
IL_003A: ldnull
IL_003B: cgt.un
IL_003D: ldc.i4.0
IL_003E: ceq
IL_0040: stloc.3
IL_0041: ldloc.3
IL_0042: brfalse.s IL_0051
IL_0044: ldloc.0 // user
IL_0045: ldloc.1 // value
IL_0046: unbox.any System.Int32
IL_004B: callvirt User.set_Age
IL_0050: nop
IL_0051: ldloc.0 // user
IL_0052: stloc.s 04
IL_0054: br.s IL_0056
IL_0056: ldloc.s 04
IL_0058: ret
跟Dapper生成的IL比對可以發現大致是一樣的
(差異部分後面會講解),代表兩者在運行的邏輯、效率上都會是差不多的,這也是為何Dapper效率接近原生ADO.NET
的原因之一。